package com.unknow.first.dapp.admin.account.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lianziyou.chain.common.api.CommonResult;
import com.unknow.first.dapp.admin.account.config.AddressAccountProperties;
import com.unknow.first.dapp.admin.account.constants.AccountConstants;
import com.unknow.first.dapp.admin.account.constants.ContractConstants;
import com.unknow.first.dapp.admin.account.mapper.AddressAccountInfo;
import com.unknow.first.dapp.admin.account.mapper.AddressCoinConfig;
import com.unknow.first.dapp.admin.account.mapper.AddressWithdraw;
import com.unknow.first.dapp.admin.account.mapper.AddressWithdrawMapper;
import com.unknow.first.dapp.admin.account.service.AddressAccountInfoService;
import com.unknow.first.dapp.admin.account.service.AddressCoinConfigService;
import com.unknow.first.dapp.admin.account.service.AddressWithdrawService;
import com.unknow.first.dapp.admin.account.service.WithdrawHandlerInterface;
import com.unknow.first.dapp.admin.account.utils.SystemAddressUtil;
import com.unknow.first.dapp.admin.account.utils.Web3Utils;
import com.unknownman.block.constants.LogConstants;
import com.unknownman.block.entity.TContractLogs;
import com.unknownman.block.service.TContractLogsService;
import lombok.extern.slf4j.Slf4j;
import org.cloud.common.service.AESService;
import org.cloud.exception.BusinessException;
import org.cloud.utils.SpringContextUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.Hash;
import org.web3j.crypto.Sign;
import org.web3j.protocol.Web3j;
import org.web3j.utils.Numeric;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import static com.unknow.first.dapp.admin.account.constants.AccountConstants.CoinConfigStatus.NORMAL;
import static com.unknow.first.dapp.admin.account.constants.AccountConstants.CoinConfigStatus.ONLY_WITHDRAW;

@Slf4j
@Service
public class AddressWithdrawServiceImpl extends ServiceImpl<AddressWithdrawMapper, AddressWithdraw> implements AddressWithdrawService{

    private Long chainId;
    @Autowired
    private AddressCoinConfigService coinConfigService;
    @Autowired
    private AddressAccountInfoService accountInfoService;
    @Autowired
    private AESService aesService;
    @Autowired
    private AddressAccountProperties addressAccountProperties;
    @Autowired
    private TContractLogsService contractLogsService;
    @Override
    public int insertSelective(AddressWithdraw record) {
        return baseMapper.insertSelective(record);
    }
    @Override
    public int updateByPrimaryKeySelective(AddressWithdraw record) {
        return baseMapper.updateByPrimaryKeySelective(record);
    }
    @Override
    public int updateBatch(List<AddressWithdraw> list) {
        return baseMapper.updateBatch(list);
    }
    @Override
    public int batchInsert(List<AddressWithdraw> list) {
        return baseMapper.batchInsert(list);
    }


    private BigInteger getChainId(){
        if (this.chainId == null){
            Web3j web3j = Web3Utils.single().web3j();
            this.chainId = Web3Utils.single().getChainId(web3j);
            Assert.notNull(chainId, "init chainId fail");
        }
        return BigInteger.valueOf(this.chainId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public CommonResult<AddressWithdraw> withdraw(String address, String toAddress, Integer coinId, BigDecimal amount) throws Exception {
        if (ObjectUtils.isEmpty(address) || ObjectUtils.isEmpty(toAddress)){
            return CommonResult.failed("params error");
        }
        amount = amount.setScale(18, RoundingMode.DOWN);
        //处理器
        WithdrawHandlerInterface withdrawHandlerInterface = SpringContextUtil.getBean(WithdrawHandlerInterface.class);
        String username = null;
        if (withdrawHandlerInterface != null){
            withdrawHandlerInterface.doBefore(address, coinId, amount);
            username = withdrawHandlerInterface.getUsername(address);
        }
        AddressAccountInfo addressAccountInfo = accountInfoService.getAddressAccountByCoinId(address, coinId);
        Assert.notNull(addressAccountInfo, "AddressAccount.error.accountIsNotExists");  // 账户不存在或者被禁用
        Assert.isTrue(addressAccountInfo.getStatus() == AccountConstants.AccountStatus.NORMAL.status, "AddressAccount.error.accountIsDisabled");  // 账户不存在或者被禁用
        final BigDecimal addressBalance = addressAccountInfo.getBalance();
        // 防止js的16位精度问题，如果提现额大于当前余额，那么按当前余额全部提现
        if (amount.compareTo(addressBalance) > 0) {
            log.warn("[{}]用户提现-[, 原始提现金额:{},超出用户余额{}]，按用户余额全部提现！", address, amount, addressBalance);
            amount = addressBalance;
        }
        // 查询当前币种信息
        AddressCoinConfig coin = coinConfigService.getByCoinId(coinId);
        Assert.isTrue(coin != null
                && (coin.getStatus() == ONLY_WITHDRAW.status || coin.getStatus() == NORMAL.status),
                    "addressAccount.error.coinIdNoExistsOrCanNotWithDraw");
        BigDecimal feeAmount;
        if (AccountConstants.WithdrawChargeType.BY_PERCENT.value.equals(coin.getWithdrawChargeType())) {
            feeAmount = amount.multiply(coin.getWithdrawCharge()).setScale(18, RoundingMode.HALF_UP);
        } else if (AccountConstants.WithdrawChargeType.BY_FIX.value.equals(coin.getWithdrawChargeType())) {
            feeAmount = coin.getWithdrawCharge();
        } else {
            throw new BusinessException("address.account.error.charge.type.notSupport");  // 不支持的手续费类型
        }
        //实际可转出金额
        BigDecimal realAmount = amount.subtract(feeAmount);
        if (realAmount.compareTo(BigDecimal.ZERO) <= 0){
            return CommonResult.failed("Too small a amount");
        }
        //默认自动审核
        AccountConstants.WithdrawStatus withdrawStatus = AccountConstants.WithdrawStatus.AUDIT_PASS;
        //触发人工审核阈值
        if (coin.getWithdrawAuthLimit() != null && amount.compareTo(coin.getWithdrawAuthLimit()) >= 0) {
            withdrawStatus = AccountConstants.WithdrawStatus.AUDITING;
        }
        String orderNo = Hash.sha3String(Numeric.toHexString(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)));
        //封装提现记录
        AddressWithdraw addressWithdraw = AddressWithdraw.builder()
                .address(address)
                .username(username)
                .toAddress(toAddress)
                .coinId(coin.getCoinId())
                .coinSymbol(coin.getCoinSymbol())
                .amount(amount)
                .fee(feeAmount)
                .realAmount(realAmount)
                .orderNo(orderNo)
                .status(withdrawStatus.value)
                .build();

        //审核通过
        if (withdrawStatus.equals(AccountConstants.WithdrawStatus.AUDIT_PASS)){
            //签名
            JSONObject signOrderData = signOrderData(toAddress, realAmount, orderNo, coin);
            addressWithdraw.setSignData(signOrderData.toJSONString());
        }
        //保存提现记录
        int insertSelective = getBaseMapper().insertSelective(addressWithdraw);
        Assert.isTrue(insertSelective > 0, "save fail");
        //待审核
        if (AccountConstants.WithdrawStatus.AUDITING.equals(withdrawStatus)) {
            JSONObject remark = new JSONObject();
            remark.put(AccountConstants.TradeRemarkColumn.DESCRIPTION.code, "提现冻结");
            accountInfoService.processFundAndDetail(BigDecimal.ZERO.subtract(amount), coin, AccountConstants.TradeType.FREE_BALANCE,
                    remark.toJSONString(), addressWithdraw.getId(), address, "System");
            accountInfoService.processFundAndDetail(amount, coin, AccountConstants.TradeType.FREE,
                    remark.toJSONString(), addressWithdraw.getId(), address, "System");
        }else { //自动转出
            //实际提现部分
            accountInfoService.processFundAndDetail(BigDecimal.ZERO.subtract(realAmount), coin, AccountConstants.TradeType.WITHDRAW,
                    "{}", addressWithdraw.getId(), address, "System");
            //实际手续费部分
            if (feeAmount.compareTo(BigDecimal.ZERO) > 0){
                String chargeAccount = SystemAddressUtil.single().getFirstSystemAddress(AccountConstants.SystemAddressType.SERVICE_CHARGE);
                JSONObject chargeJsonObject = new JSONObject();
                chargeJsonObject.put(AccountConstants.TradeRemarkColumn.PAY_TO.code, chargeAccount);
                //扣手续费
                accountInfoService.processFundAndDetail(BigDecimal.ZERO.subtract(feeAmount), coin, AccountConstants.TradeType.WITHDRAW_CHARGE,
                        chargeJsonObject.toJSONString(), addressWithdraw.getId(), address, "System");
                //系统账户加手续费
                chargeJsonObject.remove(AccountConstants.TradeRemarkColumn.PAY_TO.code);
                chargeJsonObject.put(AccountConstants.TradeRemarkColumn.PAY_BY.code, address);
                accountInfoService.processFundAndDetail(feeAmount, coin, AccountConstants.TradeType.WITHDRAW_CHARGE,
                        chargeJsonObject.toJSONString(), addressWithdraw.getId(), chargeAccount, "System");
            }
        }
        if (withdrawHandlerInterface != null){
            withdrawHandlerInterface.doAfter(addressWithdraw);
        }
        return CommonResult.success(addressWithdraw);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public CommonResult<?> adminAudit(Long id, AccountConstants.WithdrawStatus withdrawStatus, String remark) throws Exception {
        if (withdrawStatus.value != AccountConstants.WithdrawStatus.AUDIT_PASS.value
                && withdrawStatus.value != AccountConstants.WithdrawStatus.AUDIT_REFUSE.value){
            return CommonResult.failed("无效的操作状态");
        }
        AddressWithdraw addressWithdraw = getById(id);
        if (addressWithdraw.getStatus() != AccountConstants.WithdrawStatus.AUDITING.value){
            return CommonResult.failed("订单状态不可操作");
        }
        AddressCoinConfig coin = coinConfigService.getByCoinId(addressWithdraw.getCoinId());
        if (withdrawStatus.value == AccountConstants.WithdrawStatus.AUDIT_PASS.value){  //审核通过
            //提现签名
            JSONObject signOrderData = signOrderData(addressWithdraw.getToAddress(), addressWithdraw.getRealAmount(), addressWithdraw.getOrderNo(), coin);
            int updateStatusAuditPass = getBaseMapper().updateStatusAuditPass(id, signOrderData.toJSONString(), remark);
            Assert.isTrue(updateStatusAuditPass > 0, "更新失败");
            //解冻资金
            JSONObject accountDetailRemark = new JSONObject();
            accountDetailRemark.put(AccountConstants.TradeRemarkColumn.DESCRIPTION.code, "提现通过解冻");
            accountInfoService.processFundAndDetail(BigDecimal.ZERO.subtract(addressWithdraw.getAmount()), coin, AccountConstants.TradeType.UNFREE,
                    accountDetailRemark.toJSONString(), addressWithdraw.getId(), addressWithdraw.getAddress(), "System");
            accountInfoService.processFundAndDetail(addressWithdraw.getAmount(), coin, AccountConstants.TradeType.UNFREE_BALANCE,
                    accountDetailRemark.toJSONString(), addressWithdraw.getId(), addressWithdraw.getAddress(), "System");
            //实际提现部分
            accountInfoService.processFundAndDetail(BigDecimal.ZERO.subtract(addressWithdraw.getRealAmount()), coin, AccountConstants.TradeType.WITHDRAW,
                    "{}", addressWithdraw.getId(), addressWithdraw.getAddress(), "System");
            //实际手续费部分
            if (addressWithdraw.getFee().compareTo(BigDecimal.ZERO) > 0){
                String chargeAccount = SystemAddressUtil.single().getFirstSystemAddress(AccountConstants.SystemAddressType.SERVICE_CHARGE);
                JSONObject chargeJsonObject = new JSONObject();
                chargeJsonObject.put(AccountConstants.TradeRemarkColumn.PAY_TO.code, chargeAccount);
                //扣手续费
                accountInfoService.processFundAndDetail(BigDecimal.ZERO.subtract(addressWithdraw.getFee()), coin, AccountConstants.TradeType.WITHDRAW_CHARGE,
                        chargeJsonObject.toJSONString(), addressWithdraw.getId(), addressWithdraw.getAddress(), "System");
                //系统账户加手续费
                chargeJsonObject.remove(AccountConstants.TradeRemarkColumn.PAY_TO.code);
                chargeJsonObject.put(AccountConstants.TradeRemarkColumn.PAY_BY.code, addressWithdraw.getAddress());
                accountInfoService.processFundAndDetail(addressWithdraw.getFee(), coin, AccountConstants.TradeType.WITHDRAW_CHARGE,
                        chargeJsonObject.toJSONString(), addressWithdraw.getId(), chargeAccount, "System");
            }
        } else {    //审核拒绝
            int updateStatusAuditPass = getBaseMapper().updateStatusAuditRefuse(id, remark);
            Assert.isTrue(updateStatusAuditPass > 0, "更新失败");
            //解冻资金
            JSONObject accountDetailRemark = new JSONObject();
            accountDetailRemark.put(AccountConstants.TradeRemarkColumn.DESCRIPTION.code, "提现拒绝解冻");
            accountInfoService.processFundAndDetail(BigDecimal.ZERO.subtract(addressWithdraw.getAmount()), coin, AccountConstants.TradeType.UNFREE,
                    accountDetailRemark.toJSONString(), addressWithdraw.getId(), addressWithdraw.getAddress(), "System");
            accountInfoService.processFundAndDetail(addressWithdraw.getAmount(), coin, AccountConstants.TradeType.UNFREE_BALANCE,
                    accountDetailRemark.toJSONString(), addressWithdraw.getId(), addressWithdraw.getAddress(), "System");
        }
        return CommonResult.success();
    }

    @Override
    public void processWithdrawStatusDone() {
        List<TContractLogs> logsList = contractLogsService.list(
                Wrappers.lambdaQuery(TContractLogs.class)
                        .eq(TContractLogs::getStatus, LogConstants.ContractLogStatus.UNTREATED.getStatus())
                        .eq(TContractLogs::getFirstTopic, ContractConstants.Event.WITHDRAW.topic0)
        );
        if (ObjectUtils.isEmpty(logsList)){
            return;
        }
        for (TContractLogs contractLogs : logsList) {
            String orderNo = contractLogs.getThirdTopic();
            getBaseMapper().updateStatusDoneByOrderNo(orderNo, contractLogs.getContractLogId(),
                    contractLogs.getTransHash(), contractLogs.getBlockTimestamp());
            contractLogsService.updateStatusSuccess(contractLogs.getContractLogId());
        }
    }

    @Override
    public void processWithdrawStatusDoneBack() throws IOException {
        long currentTimeMillis = System.currentTimeMillis();
        //五分钟前的时间
        Date precedingDate = new Date(currentTimeMillis - 1000 * 60 * 5);

        //仅处理通过审核五分钟还没完成的订单
        List<AddressWithdraw> list = list(
                Wrappers.lambdaQuery(AddressWithdraw.class)
                        .eq(AddressWithdraw::getStatus, AccountConstants.WithdrawStatus.AUDIT_PASS.value)
                        .lt(AddressWithdraw::getUpdatedDate, precedingDate)
        );
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        Web3j web3j = Web3Utils.single().web3j();
        for (AddressWithdraw addressWithdraw : list) {
            String contractAddress = JSON.parseObject(addressWithdraw.getSignData()).getString("contractAddress");
            boolean orderStatus = Web3Utils.single().getETHWithdrawOrderStatus(web3j, contractAddress, addressWithdraw.getOrderNo());
            if (orderStatus) {
                getBaseMapper().updateStatusDoneByOrderNo(addressWithdraw.getOrderNo(), null, null, null);
            }
        }
    }

    @NotNull
    private JSONObject signOrderData(String address, BigDecimal amount, String orderNo, AddressCoinConfig addressCoinConfig) throws Exception {

        String contractAddress = addressCoinConfig.getPayContract();
        BigInteger bigAmount = amount.multiply(BigDecimal.TEN.pow(addressCoinConfig.getCoinDecimals()))
                .toBigInteger();
        String messageStr =
                "0x" + Numeric.toHexStringNoPrefixZeroPadded(new BigInteger(contractAddress.substring(2), 16), 64) + Numeric.toHexStringNoPrefixZeroPadded(
                        new BigInteger(address.substring(2), 16), 64)
                        + orderNo.substring(2) + Numeric.toHexStringNoPrefixZeroPadded(bigAmount, 64) + Numeric.toHexStringNoPrefixZeroPadded(
                        getChainId(), 64);
        Credentials credentials = Credentials.create(aesService.decrypt(addressAccountProperties.getWithdrawPrivateKey()));
        Sign.SignatureData signatureData = Sign.signMessage(Numeric.hexStringToByteArray(messageStr), credentials.getEcKeyPair());
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("contractAddress", contractAddress);
        jsonObject.put("payTo", address);
        jsonObject.put("orderNo", orderNo);
        jsonObject.put("amount", bigAmount.toString());
        jsonObject.put("v", Numeric.toBigInt(signatureData.getV()));
        jsonObject.put("r", Numeric.toHexString(signatureData.getR()));
        jsonObject.put("s", Numeric.toHexString(signatureData.getS()));
        return jsonObject;
    }
}
